This notebook is all about benchmarking some R code used in this package.
Hardware / Software used:
- Intel i7-4600U
- Compilation flags for C/C++:
-O2 -Wall $(DEBUGFLAG) -mtune=core2 (R’s defaults)
- Windows Server 2012 R2
- R 3.3.2 + Intel MKL
Libraries
library(data.table)
data.table 1.10.4
The fastest way to learn (by data.table authors): https://www.datacamp.com/courses/data-analysis-the-data-table-way
Documentation: ?data.table, example(data.table) and browseVignettes("data.table")
Release notes, videos and slides: http://r-datatable.com
library(microbenchmark)
library(Rcpp)
library(ggplot2)
library(plotly)
Attaching package: <U+393C><U+3E31>plotly<U+393C><U+3E32>
The following object is masked from <U+393C><U+3E31>package:ggplot2<U+393C><U+3E32>:
last_plot
The following object is masked from <U+393C><U+3E31>package:stats<U+393C><U+3E32>:
filter
The following object is masked from <U+393C><U+3E31>package:graphics<U+393C><U+3E32>:
layout
# Helper function to print data well in tables
print_well <- function(data, digits = 6) {
# To milliseconds
data <- data / 1000000
# Sprintf helper
sprintf_helper <- paste0("%.0", digits, "f")
cat("| Min | 25% | 50% | 75% | Max | Mean | \n| --: | --: | --: | --: | --: | --: | \n| ", sprintf(sprintf_helper, min(data)), " | ", sprintf(sprintf_helper, quantile(data, probs = 0.25)), " | ", sprintf(sprintf_helper, median(data)), " | ", sprintf(sprintf_helper, quantile(data, probs = 0.75)), " | ", sprintf(sprintf_helper, max(data)), " | ", sprintf(sprintf_helper, mean(data)), " | \n", sep = "")
return(data)
}
# Test case function
# Arguments renamed to avoid recursive clash
test_case <- function(f, preds, labels, eps) {
cat("Test case: ", paste(do.call(f, list(preds = preds[1:50],
labels = labels[1:5],
eps = 1e-15)), collapse = ", "), " \n", sep = "")
}
# Fastest Logloss function
cppFunction("double Lpp_logloss1(NumericVector preds, NumericVector labels, double eps) {
NumericVector clamped = clamp(eps, preds, 1 - eps);
NumericVector loggy = -log(1 - clamped);
double logloss = sum(loggy) / loggy.size();
return logloss;
}")
# Fastest Transformer function
cppFunction("NumericVector Lpp_vec2mat2vec(NumericVector preds, NumericVector labels) {
int labels_size = labels.size();
NumericVector selected(labels_size);
selected = (preds.size() / labels_size) * seq(0, labels_size - 1);
selected = selected + labels;
NumericVector to_return(labels_size);
to_return = preds[selected];
return to_return;
}")
Benchmarking Clamped Vector to Logloss
For a 10-class vector of 1,000,000 observations:
- Vector A of length=(1000000 * 10)
- Vector B of length=(1000000) with 10 classes
A = [1:1, 1:2, 1:3, 1:4... 1:10, 2:1, 2:2, 2:3..., 1000000:8, 1000000:9, 1000000:10]
B = [3, 5, 9, 1, 4, 8, 6, ...]
Get the following Vector C, D, and E:
C = [1:4, 2:6, 3:10, 4:2, 5:5, 6:9, 7:7, ...]
D = Clamped C by 1e-15
E = Mean of logloss(D, B)
Initialize data
# Generate random data
set.seed(11111)
data <- runif(10000000, 0, 1)
labels <- round(runif(1000000, 0, 9), digits = 0)
# Background truth example (no clamping though)
array(data[1:50], dim = c(10, 5))
[,1] [,2] [,3] [,4] [,5]
[1,] 0.5014483 0.57219649 0.70236924 0.06440384 0.78924978
[2,] 0.9702328 0.34292525 0.50889166 0.65780657 0.72449914
[3,] 0.7876004 0.09627503 0.20268701 0.19973930 0.00886554
[4,] 0.9022259 0.74235690 0.90706612 0.24664551 0.57348710
[5,] 0.8141778 0.42274539 0.05441064 0.25997440 0.73034459
[6,] 0.7998922 0.98402494 0.09045308 0.14869691 0.42458612
[7,] 0.1158690 0.89258437 0.85759800 0.21375685 0.07684022
[8,] 0.7171363 0.59541565 0.94780036 0.20598170 0.53318628
[9,] 0.1020639 0.80531234 0.16479666 0.02075780 0.28521238
[10,] 0.6856938 0.42102379 0.42536097 0.63880218 0.86408083
labels[1:5]
[1] 5 1 1 7 8
data[c(1 + labels[1], 11 + labels[2], 21 + labels[3], 31 + labels[4], 41 + labels[5])]
[1] 0.7998922 0.3429253 0.5088917 0.2059817 0.2852124
-log(1 - data[c(1 + labels[1], 11 + labels[2], 21 + labels[3], 31 + labels[4], 41 + labels[5])])
[1] 1.6088991 0.4199575 0.7110905 0.2306488 0.3357698
mean(-log(1 - data[c(1 + labels[1], 11 + labels[2], 21 + labels[3], 31 + labels[4], 41 + labels[5])]))
[1] 0.6612731
# How many digits for benchmarking in milliseconds
my_digits <- 6L
# How many runs for benchmarking?
my_runs <- 1000L
Benchmarks
# ===== BLOCK 1 =====
faster1 <- function(preds, labels, eps = 1e-15) {
temp_preds <- Lpp_vec2mat2vec(preds, labels)
temp_log <- Lpp_logloss1(temp_preds, labels, eps)
return(temp_log)
}
test_case(faster1, preds = data, labels = labels, eps = 1e-15)
Test case: 0.661273148685013
data1 <- print_well(microbenchmark(faster1(data, labels), times = my_runs)$time, digits = my_digits)
| 68.776904 |
74.052150 |
77.595495 |
80.546207 |
158.218773 |
79.329577 |
# ===== BLOCK 2 =====
faster2 <- function(preds, labels, eps = 1e-15) {
x <- pmin(pmax(preds[((0:(length(labels) - 1)) * (length(preds) / length(labels))) + labels + 1], eps), 1 - eps)
return(-mean(log(1 - x)))
}
test_case(faster2, preds = data, labels = labels, eps = 1e-15)
Test case: 0.661273148685013
data2 <- print_well(microbenchmark(faster2(data, labels), times = my_runs)$time, digits = my_digits)
| 76.919802 |
83.673872 |
87.017170 |
90.827465 |
168.569882 |
90.820915 |
# ===== BLOCK 3 =====
faster3 <- function(preds, labels, eps = 1e-15) {
x <- pmin(pmax(preds[((0:(length(labels) - 1)) * (length(preds) / length(labels))) + labels + 1], eps), 1 - eps)
return(-sum(log(1 - x)) / length(labels))
}
test_case(faster3, preds = data, labels = labels, eps = 1e-15)
Test case: 0.661273148685013
data3 <- print_well(microbenchmark(faster3(data, labels), times = my_runs)$time, digits = my_digits)
| 76.257605 |
83.326427 |
85.514111 |
88.191601 |
170.386934 |
90.041468 |
# ===== BLOCK 4 =====
cppFunction("double faster4(NumericVector preds, NumericVector labels, double eps) {
int labels_size = labels.size();
NumericVector selected(labels_size);
selected = (preds.size() / labels_size) * seq(0, labels_size - 1);
selected = selected + labels;
NumericVector to_return(labels_size);
to_return = preds[selected];
NumericVector clamped = clamp(eps, to_return, 1 - eps);
NumericVector loggy = -(log(1 - clamped));
double logloss = sum(loggy) / labels_size;
return logloss;
}")
test_case(faster4, preds = data, labels = labels, eps = 1e-15)
Test case: 0.661273148685013
data4 <- print_well(microbenchmark(faster4(data, labels, eps = 1e-15), times = my_runs)$time, digits = my_digits)
| 68.158803 |
70.104625 |
79.027087 |
81.212965 |
155.654754 |
77.006338 |
Summary Results
data_time <- data.table(rbindlist(list(data.frame(Time = data1, Bench = "faster1"),
data.frame(Time = data2, Bench = "faster2"),
data.frame(Time = data3, Bench = "faster3"),
data.frame(Time = data4, Bench = "faster4"))))
data_time <- data_time[, t_mean := mean(Time), by = Bench]
data_time <- data_time[, t_median := median(Time), by = Bench]
data_time$Benchs <- data_time$Bench
levels(data_time$Benchs) <- paste0("faster", 1:4, "= [", sprintf(paste0("%.0", my_digits, "f"), data_time[, list(min(Time)), by = Bench]$V1), ", ", sprintf(paste0("%.0", my_digits, "f"), data_time[, list(max(Time)), by = Bench]$V1), "], mean=", sprintf(paste0("%.0", my_digits, "f"), data_time[, list(mean(Time)), by = Bench]$V1), ", median=", sprintf(paste0("%.0", my_digits, "f"), data_time[, list(median(Time)), by = Bench]$V1))
my_time <- data_time[, list(min(Time), quantile(Time, probs = 0.25), median(Time), quantile(Time, probs = 0.75), max(Time), mean(Time)), by = Bench]
colnames(my_time) <- c("Function", "Min", "25%", "50%", "75%", "Max", "Mean")
my_time <- my_time[order(Mean, decreasing = FALSE), ]
print(my_time, digits = 6)
Plot Results
ggplotly(ggplot(data = data_time, aes(x = Time)) + geom_histogram(aes(y = ..density..), bins = 20, color = "darkblue", fill = "lightblue") + facet_wrap(~ Benchs, ncol = 2) + geom_vline(aes(xintercept = t_mean), color = "blue", linetype = "dashed", size = 2) + geom_vline(aes(xintercept = t_median), color = "red", linetype = "dashed", size = 2) + labs(x = "Time (Milliseconds)", y = "Density") + theme_bw())
ggplotly(ggplot(data = data_time[, .(Time, Bench)], aes(x = Time, y = ..count.., fill = Bench)) + geom_histogram(aes(y = ..density..), bins = 100, position = "fill") + labs(x = "Time (Milliseconds)", y = "Density") + theme_bw())
ggplotly(ggplot(data = data_time[, .(Time, Bench)], aes(x = Time, y = ..count.., fill = Bench)) + geom_density(position = "fill") + labs(x = "Time (Milliseconds)", y = "Density") + theme_bw())
Scaling Benchmarks
Benchmarker <- function(f, size, runs, digits, name) {
data_runs <- list()
for (i in 1:length(size)) {
set.seed(11111)
data <- runif(size[i] * 10, 0, 1)
labels <- round(runif(size[i], 0, 9), digits = 0)
cat(" \n \n## ", name, " run: ",format(size[i], big.mark = ",", scientific = FALSE), " samples (", format(runs[i], big.mark = ",", scientific = FALSE), " times) \n \n", sep = "")
test_case(f, preds = data, labels = labels, eps = 1e-15)
cat(" \n")
data_runs[[i]] <- print_well(microbenchmark(f(data, labels, eps = 1e-15), times = runs[i])$time, digits = digits)
data_runs[[i]] <- data.table(Bench = as.factor(paste0("[", i, "] ", format(size[i], big.mark = ",", scientific = FALSE))), Function = as.factor(name), Time = data_runs[[i]])
gc(verbose = FALSE)
}
return(data_runs)
}
bench_size <- c(100, 1000, 10000, 100000, 1000000, 10000000)
bench_runs <- c(10000, 5000, 1000, 500, 100, 50)
run1 <- Benchmarker(faster3, bench_size, bench_runs, my_digits, "Pure R")
Pure R run: 100 samples (10,000 times)
Test case: 1.35212558439888
| 0.022808 |
0.025469 |
0.027370 |
0.043335 |
4.638802 |
0.036441 |
Pure R run: 1,000 samples (5,000 times)
Test case: 1.11833540263107
| 0.073366 |
0.076787 |
0.083630 |
0.091613 |
3.864085 |
0.088550 |
Pure R run: 10,000 samples (1,000 times)
Test case: 0.797706755821707
| 0.586930 |
0.595673 |
0.670181 |
0.683865 |
3.591907 |
0.670329 |
Pure R run: 100,000 samples (500 times)
Test case: 1.90925095682509
| 6.245258 |
6.728032 |
7.275997 |
7.517289 |
29.690539 |
7.664803 |
Pure R run: 1,000,000 samples (100 times)
Test case: 0.661273148685013
| 64.671813 |
67.926919 |
71.717638 |
75.255851 |
151.530276 |
74.316310 |
Pure R run: 10,000,000 samples (50 times)
Test case: 0.966169328545996
| 741.884853 |
786.242472 |
809.841423 |
821.969478 |
993.530082 |
812.830624 |
run2 <- Benchmarker(faster4, bench_size, bench_runs, my_digits, "Rcpp")
Rcpp run: 100 samples (10,000 times)
Test case: 1.35212558439888
| 0.006082 |
0.006463 |
0.006843 |
0.007223 |
0.068045 |
0.007184 |
Rcpp run: 1,000 samples (5,000 times)
Test case: 1.11833540263107
| 0.051318 |
0.055120 |
0.060062 |
0.067284 |
0.210595 |
0.064723 |
Rcpp run: 10,000 samples (1,000 times)
Test case: 0.797706755821707
| 0.510143 |
0.515369 |
0.583890 |
0.593773 |
1.034731 |
0.574722 |
Rcpp run: 100,000 samples (500 times)
Test case: 1.90925095682509
| 5.590283 |
5.684082 |
5.804680 |
6.427818 |
12.891180 |
6.154242 |
Rcpp run: 1,000,000 samples (100 times)
Test case: 0.661273148685013
| 57.931618 |
59.823270 |
62.286268 |
66.095328 |
138.984261 |
63.797930 |
Rcpp run: 10,000,000 samples (50 times)
Test case: 0.966169328545996
| 673.112510 |
692.100884 |
695.991768 |
706.077447 |
800.154031 |
702.179116 |
Scaling Results
run1_all <- rbindlist(run1)
run2_all <- rbindlist(run2)
run_all <- rbind(run1_all, run2_all)
run_all$Repeats <- rep(inverse.rle(list(lengths = bench_runs, values = bench_size)), 2)
run_all$MilObs <- (1000 / run_all$Time) * run_all$Repeats / 1000000
run_time <- run_all[, list(quantile(Time, probs = 0.05), median(Time), quantile(Time, probs = 0.95), mean(Time)), by = list(Function, Bench)]
colnames(run_time) <- c("Function", "Benchmark", "5%", "50%", "95%", "Mean")
run_time$`Mil.Obs/s` <- (1000 / run_time$Mean) * bench_size / 1000000
run_time$`5%` <- format(run_time$`5%`, digits = 6, scientific = FALSE)
run_time$`50%` <- format(run_time$`50%`, digits = 6, scientific = FALSE)
run_time$`95%` <- format(run_time$`95%`, digits = 6, scientific = FALSE)
run_time$Mean <- format(run_time$Mean, digits = 6, scientific = FALSE)
run_time$`Mil.Obs/s` <- format(run_time$`Mil.Obs/s`, digits = 6, scientific = FALSE)
print(run_time[1:(nrow(run_time) / 2)])
print(run_time[(nrow(run_time) / 2 + 1):nrow(run_time)])
ggplot(run_all, aes(x = Bench, y = Time, color = Function, fill = Bench)) + geom_boxplot() + scale_y_log10(labels = scales::comma, breaks = c(0.01, 0.1, 1, 10, 100, 1000, 10000)) + stat_summary(fun.y = mean, geom = "line", aes(group = Function)) + stat_summary(fun.y = mean, geom = "point", aes(group = Function)) + labs(x = "Benchmark", y = "Time (Milliseconds)") + theme_bw()

ggplot(run_all, aes(x = Bench, y = MilObs, color = Function, fill = Bench)) + geom_boxplot() + scale_y_log10(labels = scales::comma, breaks = c(1, 2.5, 5, 7.5, 10, 12.5, 15, 17.5, 20, 22.5, 25), limits = c(1, NA)) + stat_summary(fun.y = mean, geom = "line", aes(group = Function)) + stat_summary(fun.y = mean, geom = "point", aes(group = Function)) + labs(x = "Benchmark", y = "Throughput (Million Obs./s)") + theme_bw()

ggplot(data = run_all, aes(x = MilObs, color = Function, fill = Function, group = Function)) + coord_flip() + stat_ecdf(aes(ymin = ..y.., ymax = 1), alpha = 0.5, geom = "ribbon") + stat_ecdf(geom = "line", size = 2, alpha = 0.75, pad = FALSE) + labs(x = "Throughput (Million Obs./s)", y = "Percentile") + facet_wrap(~ Bench, dir = "h", ncol = 2, scales = "free") + theme_bw()

LS0tDQp0aXRsZTogIkJlbmNobWFya3M6IE11bHRpLWNsYXNzIExvZ2xvc3MiDQpvdXRwdXQ6DQogIGh0bWxfbm90ZWJvb2s6DQogICAgY29sbGFwc2VkOiBubw0KICAgIHRoZW1lOiB1bml0ZWQNCiAgICB0b2M6IHllcw0KICAgIHRvY19kZXB0aDogMQ0KICAgIHRvY19mbG9hdDogeWVzDQotLS0NCg0KVGhpcyBub3RlYm9vayBpcyBhbGwgYWJvdXQgYmVuY2htYXJraW5nIHNvbWUgUiBjb2RlIHVzZWQgaW4gdGhpcyBwYWNrYWdlLg0KDQpIYXJkd2FyZSAvIFNvZnR3YXJlIHVzZWQ6DQoNCiogSW50ZWwgaTctNDYwMFUNCiogQ29tcGlsYXRpb24gZmxhZ3MgZm9yIEMvQysrOiBgLU8yIC1XYWxsICQoREVCVUdGTEFHKSAtbXR1bmU9Y29yZTJgIChSJ3MgZGVmYXVsdHMpDQoqIFdpbmRvd3MgU2VydmVyIDIwMTIgUjINCiogUiAzLjMuMiArIEludGVsIE1LTA0KDQojIExpYnJhcmllcw0KDQpgYGB7ciBpbml0fQ0KbGlicmFyeShkYXRhLnRhYmxlKQ0KbGlicmFyeShtaWNyb2JlbmNobWFyaykNCmxpYnJhcnkoUmNwcCkNCmxpYnJhcnkoZ2dwbG90MikNCmxpYnJhcnkocGxvdGx5KQ0KYGBgDQoNCmBgYHtyIGJhc2VkfQ0KDQojIEhlbHBlciBmdW5jdGlvbiB0byBwcmludCBkYXRhIHdlbGwgaW4gdGFibGVzDQpwcmludF93ZWxsIDwtIGZ1bmN0aW9uKGRhdGEsIGRpZ2l0cyA9IDYpIHsNCiAgDQogICMgVG8gbWlsbGlzZWNvbmRzDQogIGRhdGEgPC0gZGF0YSAvIDEwMDAwMDANCiAgDQogICMgU3ByaW50ZiBoZWxwZXINCiAgc3ByaW50Zl9oZWxwZXIgPC0gcGFzdGUwKCIlLjAiLCBkaWdpdHMsICJmIikNCiAgDQogIGNhdCgifCBNaW4gfCAyNSUgfCA1MCUgfCA3NSUgfCBNYXggfCBNZWFuIHwgIFxufCAtLTogfCAtLTogfCAtLTogfCAtLTogfCAtLTogfCAtLTogfCAgXG58ICIsIHNwcmludGYoc3ByaW50Zl9oZWxwZXIsIG1pbihkYXRhKSksICIgfCAiLCBzcHJpbnRmKHNwcmludGZfaGVscGVyLCBxdWFudGlsZShkYXRhLCBwcm9icyA9IDAuMjUpKSwgIiB8ICIsIHNwcmludGYoc3ByaW50Zl9oZWxwZXIsIG1lZGlhbihkYXRhKSksICIgfCAiLCBzcHJpbnRmKHNwcmludGZfaGVscGVyLCBxdWFudGlsZShkYXRhLCBwcm9icyA9IDAuNzUpKSwgIiB8ICIsIHNwcmludGYoc3ByaW50Zl9oZWxwZXIsIG1heChkYXRhKSksICIgfCAiLCBzcHJpbnRmKHNwcmludGZfaGVscGVyLCBtZWFuKGRhdGEpKSwgIiB8ICBcbiIsIHNlcCA9ICIiKQ0KICANCiAgcmV0dXJuKGRhdGEpDQogIA0KfQ0KDQojIFRlc3QgY2FzZSBmdW5jdGlvbg0KIyBBcmd1bWVudHMgcmVuYW1lZCB0byBhdm9pZCByZWN1cnNpdmUgY2xhc2gNCnRlc3RfY2FzZSA8LSBmdW5jdGlvbihmLCBwcmVkcywgbGFiZWxzLCBlcHMpIHsNCiAgY2F0KCJUZXN0IGNhc2U6ICIsIHBhc3RlKGRvLmNhbGwoZiwgbGlzdChwcmVkcyA9IHByZWRzWzE6NTBdLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxhYmVscyA9IGxhYmVsc1sxOjVdLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGVwcyA9IDFlLTE1KSksIGNvbGxhcHNlID0gIiwgIiksICIgIFxuIiwgc2VwID0gIiIpDQp9DQoNCiMgRmFzdGVzdCBMb2dsb3NzIGZ1bmN0aW9uDQpjcHBGdW5jdGlvbigiZG91YmxlIExwcF9sb2dsb3NzMShOdW1lcmljVmVjdG9yIHByZWRzLCBOdW1lcmljVmVjdG9yIGxhYmVscywgZG91YmxlIGVwcykgew0KICBOdW1lcmljVmVjdG9yIGNsYW1wZWQgPSBjbGFtcChlcHMsIHByZWRzLCAxIC0gZXBzKTsNCiAgTnVtZXJpY1ZlY3RvciBsb2dneSA9IC1sb2coMSAtIGNsYW1wZWQpOw0KICBkb3VibGUgbG9nbG9zcyA9IHN1bShsb2dneSkgLyBsb2dneS5zaXplKCk7DQogIHJldHVybiBsb2dsb3NzOw0KfSIpDQoNCiMgRmFzdGVzdCBUcmFuc2Zvcm1lciBmdW5jdGlvbg0KY3BwRnVuY3Rpb24oIk51bWVyaWNWZWN0b3IgTHBwX3ZlYzJtYXQydmVjKE51bWVyaWNWZWN0b3IgcHJlZHMsIE51bWVyaWNWZWN0b3IgbGFiZWxzKSB7DQogIGludCBsYWJlbHNfc2l6ZSA9IGxhYmVscy5zaXplKCk7DQogIE51bWVyaWNWZWN0b3Igc2VsZWN0ZWQobGFiZWxzX3NpemUpOw0KICBzZWxlY3RlZCA9IChwcmVkcy5zaXplKCkgLyBsYWJlbHNfc2l6ZSkgKiBzZXEoMCwgbGFiZWxzX3NpemUgLSAxKTsNCiAgc2VsZWN0ZWQgPSBzZWxlY3RlZCArIGxhYmVsczsNCiAgTnVtZXJpY1ZlY3RvciB0b19yZXR1cm4obGFiZWxzX3NpemUpOw0KICB0b19yZXR1cm4gPSBwcmVkc1tzZWxlY3RlZF07DQogIHJldHVybiB0b19yZXR1cm47DQp9IikNCg0KYGBgDQoNCiMgQmVuY2htYXJraW5nIENsYW1wZWQgVmVjdG9yIHRvIExvZ2xvc3MNCg0KRm9yIGEgMTAtY2xhc3MgdmVjdG9yIG9mIDEsMDAwLDAwMCBvYnNlcnZhdGlvbnM6DQoNCiogVmVjdG9yIEEgb2YgbGVuZ3RoPSgxMDAwMDAwICogMTApDQoqIFZlY3RvciBCIG9mIGxlbmd0aD0oMTAwMDAwMCkgd2l0aCAxMCBjbGFzc2VzDQoNCmBgYA0KQSA9IFsxOjEsIDE6MiwgMTozLCAxOjQuLi4gMToxMCwgMjoxLCAyOjIsIDI6My4uLiwgMTAwMDAwMDo4LCAxMDAwMDAwOjksIDEwMDAwMDA6MTBdDQpCID0gWzMsIDUsIDksIDEsIDQsIDgsIDYsIC4uLl0NCmBgYA0KDQpHZXQgdGhlIGZvbGxvd2luZyBWZWN0b3IgQywgRCwgYW5kIEU6DQoNCmBgYA0KQyA9IFsxOjQsIDI6NiwgMzoxMCwgNDoyLCA1OjUsIDY6OSwgNzo3LCAuLi5dDQpEID0gQ2xhbXBlZCBDIGJ5IDFlLTE1DQpFID0gTWVhbiBvZiBsb2dsb3NzKEQsIEIpDQpgYGANCg0KIyBJbml0aWFsaXplIGRhdGENCg0KYGBge3IgYmVuY2gxfQ0KIyBHZW5lcmF0ZSByYW5kb20gZGF0YQ0Kc2V0LnNlZWQoMTExMTEpDQpkYXRhIDwtIHJ1bmlmKDEwMDAwMDAwLCAwLCAxKQ0KbGFiZWxzIDwtIHJvdW5kKHJ1bmlmKDEwMDAwMDAsIDAsIDkpLCBkaWdpdHMgPSAwKQ0KDQojIEJhY2tncm91bmQgdHJ1dGggZXhhbXBsZSAobm8gY2xhbXBpbmcgdGhvdWdoKQ0KYXJyYXkoZGF0YVsxOjUwXSwgZGltID0gYygxMCwgNSkpDQpsYWJlbHNbMTo1XQ0KZGF0YVtjKDEgKyBsYWJlbHNbMV0sIDExICsgbGFiZWxzWzJdLCAyMSArIGxhYmVsc1szXSwgMzEgKyBsYWJlbHNbNF0sIDQxICsgbGFiZWxzWzVdKV0NCi1sb2coMSAtIGRhdGFbYygxICsgbGFiZWxzWzFdLCAxMSArIGxhYmVsc1syXSwgMjEgKyBsYWJlbHNbM10sIDMxICsgbGFiZWxzWzRdLCA0MSArIGxhYmVsc1s1XSldKQ0KbWVhbigtbG9nKDEgLSBkYXRhW2MoMSArIGxhYmVsc1sxXSwgMTEgKyBsYWJlbHNbMl0sIDIxICsgbGFiZWxzWzNdLCAzMSArIGxhYmVsc1s0XSwgNDEgKyBsYWJlbHNbNV0pXSkpDQoNCiMgSG93IG1hbnkgZGlnaXRzIGZvciBiZW5jaG1hcmtpbmcgaW4gbWlsbGlzZWNvbmRzDQpteV9kaWdpdHMgPC0gNkwNCg0KIyBIb3cgbWFueSBydW5zIGZvciBiZW5jaG1hcmtpbmc/DQpteV9ydW5zIDwtIDEwMDBMDQpgYGANCg0KIyBCZW5jaG1hcmtzDQoNCmBgYHtyIGJlbmNoMiwgcmVzdWx0cz0iYXNpcyJ9DQoNCiMgPT09PT0gQkxPQ0sgMSA9PT09PQ0KZmFzdGVyMSA8LSBmdW5jdGlvbihwcmVkcywgbGFiZWxzLCBlcHMgPSAxZS0xNSkgew0KICB0ZW1wX3ByZWRzIDwtIExwcF92ZWMybWF0MnZlYyhwcmVkcywgbGFiZWxzKQ0KICB0ZW1wX2xvZyA8LSBMcHBfbG9nbG9zczEodGVtcF9wcmVkcywgbGFiZWxzLCBlcHMpDQogIHJldHVybih0ZW1wX2xvZykNCn0NCnRlc3RfY2FzZShmYXN0ZXIxLCBwcmVkcyA9IGRhdGEsIGxhYmVscyA9IGxhYmVscywgZXBzID0gMWUtMTUpDQpkYXRhMSA8LSBwcmludF93ZWxsKG1pY3JvYmVuY2htYXJrKGZhc3RlcjEoZGF0YSwgbGFiZWxzKSwgdGltZXMgPSBteV9ydW5zKSR0aW1lLCBkaWdpdHMgPSBteV9kaWdpdHMpDQoNCiMgPT09PT0gQkxPQ0sgMiA9PT09PQ0KZmFzdGVyMiA8LSBmdW5jdGlvbihwcmVkcywgbGFiZWxzLCBlcHMgPSAxZS0xNSkgew0KICB4IDwtIHBtaW4ocG1heChwcmVkc1soKDA6KGxlbmd0aChsYWJlbHMpIC0gMSkpICogKGxlbmd0aChwcmVkcykgLyBsZW5ndGgobGFiZWxzKSkpICsgbGFiZWxzICsgMV0sIGVwcyksIDEgLSBlcHMpDQogIHJldHVybigtbWVhbihsb2coMSAtIHgpKSkNCn0NCnRlc3RfY2FzZShmYXN0ZXIyLCBwcmVkcyA9IGRhdGEsIGxhYmVscyA9IGxhYmVscywgZXBzID0gMWUtMTUpDQpkYXRhMiA8LSBwcmludF93ZWxsKG1pY3JvYmVuY2htYXJrKGZhc3RlcjIoZGF0YSwgbGFiZWxzKSwgdGltZXMgPSBteV9ydW5zKSR0aW1lLCBkaWdpdHMgPSBteV9kaWdpdHMpDQoNCiMgPT09PT0gQkxPQ0sgMyA9PT09PQ0KZmFzdGVyMyA8LSBmdW5jdGlvbihwcmVkcywgbGFiZWxzLCBlcHMgPSAxZS0xNSkgew0KICB4IDwtIHBtaW4ocG1heChwcmVkc1soKDA6KGxlbmd0aChsYWJlbHMpIC0gMSkpICogKGxlbmd0aChwcmVkcykgLyBsZW5ndGgobGFiZWxzKSkpICsgbGFiZWxzICsgMV0sIGVwcyksIDEgLSBlcHMpDQogIHJldHVybigtc3VtKGxvZygxIC0geCkpIC8gbGVuZ3RoKGxhYmVscykpDQp9DQp0ZXN0X2Nhc2UoZmFzdGVyMywgcHJlZHMgPSBkYXRhLCBsYWJlbHMgPSBsYWJlbHMsIGVwcyA9IDFlLTE1KQ0KZGF0YTMgPC0gcHJpbnRfd2VsbChtaWNyb2JlbmNobWFyayhmYXN0ZXIzKGRhdGEsIGxhYmVscyksIHRpbWVzID0gbXlfcnVucykkdGltZSwgZGlnaXRzID0gbXlfZGlnaXRzKQ0KDQojID09PT09IEJMT0NLIDQgPT09PT0NCmNwcEZ1bmN0aW9uKCJkb3VibGUgZmFzdGVyNChOdW1lcmljVmVjdG9yIHByZWRzLCBOdW1lcmljVmVjdG9yIGxhYmVscywgZG91YmxlIGVwcykgew0KICBpbnQgbGFiZWxzX3NpemUgPSBsYWJlbHMuc2l6ZSgpOw0KICBOdW1lcmljVmVjdG9yIHNlbGVjdGVkKGxhYmVsc19zaXplKTsNCiAgc2VsZWN0ZWQgPSAocHJlZHMuc2l6ZSgpIC8gbGFiZWxzX3NpemUpICogc2VxKDAsIGxhYmVsc19zaXplIC0gMSk7DQogIHNlbGVjdGVkID0gc2VsZWN0ZWQgKyBsYWJlbHM7DQogIE51bWVyaWNWZWN0b3IgdG9fcmV0dXJuKGxhYmVsc19zaXplKTsNCiAgdG9fcmV0dXJuID0gcHJlZHNbc2VsZWN0ZWRdOw0KICBOdW1lcmljVmVjdG9yIGNsYW1wZWQgPSBjbGFtcChlcHMsIHRvX3JldHVybiwgMSAtIGVwcyk7DQogIE51bWVyaWNWZWN0b3IgbG9nZ3kgPSAtKGxvZygxIC0gY2xhbXBlZCkpOw0KICBkb3VibGUgbG9nbG9zcyA9IHN1bShsb2dneSkgLyBsYWJlbHNfc2l6ZTsNCiAgcmV0dXJuIGxvZ2xvc3M7DQp9IikNCnRlc3RfY2FzZShmYXN0ZXI0LCBwcmVkcyA9IGRhdGEsIGxhYmVscyA9IGxhYmVscywgZXBzID0gMWUtMTUpDQpkYXRhNCA8LSBwcmludF93ZWxsKG1pY3JvYmVuY2htYXJrKGZhc3RlcjQoZGF0YSwgbGFiZWxzLCBlcHMgPSAxZS0xNSksIHRpbWVzID0gbXlfcnVucykkdGltZSwgZGlnaXRzID0gbXlfZGlnaXRzKQ0KDQpgYGANCg0KIyBTdW1tYXJ5IFJlc3VsdHMNCg0KYGBge3IgYmVuY2gzfQ0KDQpkYXRhX3RpbWUgPC0gZGF0YS50YWJsZShyYmluZGxpc3QobGlzdChkYXRhLmZyYW1lKFRpbWUgPSBkYXRhMSwgQmVuY2ggPSAiZmFzdGVyMSIpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZGF0YS5mcmFtZShUaW1lID0gZGF0YTIsIEJlbmNoID0gImZhc3RlcjIiKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRhdGEuZnJhbWUoVGltZSA9IGRhdGEzLCBCZW5jaCA9ICJmYXN0ZXIzIiksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkYXRhLmZyYW1lKFRpbWUgPSBkYXRhNCwgQmVuY2ggPSAiZmFzdGVyNCIpKSkpDQpkYXRhX3RpbWUgPC0gZGF0YV90aW1lWywgdF9tZWFuIDo9IG1lYW4oVGltZSksIGJ5ID0gQmVuY2hdDQpkYXRhX3RpbWUgPC0gZGF0YV90aW1lWywgdF9tZWRpYW4gOj0gbWVkaWFuKFRpbWUpLCBieSA9IEJlbmNoXQ0KZGF0YV90aW1lJEJlbmNocyA8LSBkYXRhX3RpbWUkQmVuY2ggDQpsZXZlbHMoZGF0YV90aW1lJEJlbmNocykgPC0gcGFzdGUwKCJmYXN0ZXIiLCAxOjQsICI9IFsiLCBzcHJpbnRmKHBhc3RlMCgiJS4wIiwgbXlfZGlnaXRzLCAiZiIpLCBkYXRhX3RpbWVbLCBsaXN0KG1pbihUaW1lKSksIGJ5ID0gQmVuY2hdJFYxKSwgIiwgIiwgc3ByaW50ZihwYXN0ZTAoIiUuMCIsIG15X2RpZ2l0cywgImYiKSwgZGF0YV90aW1lWywgbGlzdChtYXgoVGltZSkpLCBieSA9IEJlbmNoXSRWMSksICJdLCBtZWFuPSIsIHNwcmludGYocGFzdGUwKCIlLjAiLCBteV9kaWdpdHMsICJmIiksIGRhdGFfdGltZVssIGxpc3QobWVhbihUaW1lKSksIGJ5ID0gQmVuY2hdJFYxKSwgIiwgbWVkaWFuPSIsIHNwcmludGYocGFzdGUwKCIlLjAiLCBteV9kaWdpdHMsICJmIiksIGRhdGFfdGltZVssIGxpc3QobWVkaWFuKFRpbWUpKSwgYnkgPSBCZW5jaF0kVjEpKQ0KDQpteV90aW1lIDwtIGRhdGFfdGltZVssIGxpc3QobWluKFRpbWUpLCBxdWFudGlsZShUaW1lLCBwcm9icyA9IDAuMjUpLCBtZWRpYW4oVGltZSksIHF1YW50aWxlKFRpbWUsIHByb2JzID0gMC43NSksIG1heChUaW1lKSwgbWVhbihUaW1lKSksIGJ5ID0gQmVuY2hdDQpjb2xuYW1lcyhteV90aW1lKSA8LSBjKCJGdW5jdGlvbiIsICJNaW4iLCAiMjUlIiwgIjUwJSIsICI3NSUiLCAiTWF4IiwgIk1lYW4iKQ0KbXlfdGltZSA8LSBteV90aW1lW29yZGVyKE1lYW4sIGRlY3JlYXNpbmcgPSBGQUxTRSksIF0NCnByaW50KG15X3RpbWUsIGRpZ2l0cyA9IDYpDQoNCmBgYA0KDQojIFBsb3QgUmVzdWx0cw0KDQpgYGB7ciBiZW5jaDQsIGZpZy5oZWlnaHQ9NiwgZmlnLndpZHRoPTEwfQ0KDQpnZ3Bsb3RseShnZ3Bsb3QoZGF0YSA9IGRhdGFfdGltZSwgYWVzKHggPSBUaW1lKSkgKyBnZW9tX2hpc3RvZ3JhbShhZXMoeSA9IC4uZGVuc2l0eS4uKSwgYmlucyA9IDIwLCBjb2xvciA9ICJkYXJrYmx1ZSIsIGZpbGwgPSAibGlnaHRibHVlIikgKyBmYWNldF93cmFwKH4gQmVuY2hzLCBuY29sID0gMikgKyBnZW9tX3ZsaW5lKGFlcyh4aW50ZXJjZXB0ID0gdF9tZWFuKSwgY29sb3IgPSAiYmx1ZSIsIGxpbmV0eXBlID0gImRhc2hlZCIsIHNpemUgPSAyKSArIGdlb21fdmxpbmUoYWVzKHhpbnRlcmNlcHQgPSB0X21lZGlhbiksIGNvbG9yID0gInJlZCIsIGxpbmV0eXBlID0gImRhc2hlZCIsIHNpemUgPSAyKSArIGxhYnMoeCA9ICJUaW1lIChNaWxsaXNlY29uZHMpIiwgeSA9ICJEZW5zaXR5IikgKyB0aGVtZV9idygpKQ0KDQpgYGANCg0KYGBge3IgYmVuY2g1LCBmaWcuaGVpZ2h0PTYsIGZpZy53aWR0aD0xMH0NCmdncGxvdGx5KGdncGxvdChkYXRhID0gZGF0YV90aW1lWywgLihUaW1lLCBCZW5jaCldLCBhZXMoeCA9IFRpbWUsIHkgPSAuLmNvdW50Li4sIGZpbGwgPSBCZW5jaCkpICsgZ2VvbV9oaXN0b2dyYW0oYWVzKHkgPSAuLmRlbnNpdHkuLiksIGJpbnMgPSAxMDAsIHBvc2l0aW9uID0gImZpbGwiKSArIGxhYnMoeCA9ICJUaW1lIChNaWxsaXNlY29uZHMpIiwgeSA9ICJEZW5zaXR5IikgKyB0aGVtZV9idygpKQ0KYGBgDQoNCmBgYHtyIGJlbmNoNiwgZmlnLmhlaWdodD02LCBmaWcud2lkdGg9MTB9DQpnZ3Bsb3RseShnZ3Bsb3QoZGF0YSA9IGRhdGFfdGltZVssIC4oVGltZSwgQmVuY2gpXSwgYWVzKHggPSBUaW1lLCB5ID0gLi5jb3VudC4uLCBmaWxsID0gQmVuY2gpKSArIGdlb21fZGVuc2l0eShwb3NpdGlvbiA9ICJmaWxsIikgKyBsYWJzKHggPSAiVGltZSAoTWlsbGlzZWNvbmRzKSIsIHkgPSAiRGVuc2l0eSIpICsgdGhlbWVfYncoKSkNCmBgYA0KDQojIFNjYWxpbmcgQmVuY2htYXJrcw0KDQpgYGB7ciBiZW5jaF9zY2FsZTF9DQoNCkJlbmNobWFya2VyIDwtIGZ1bmN0aW9uKGYsIHNpemUsIHJ1bnMsIGRpZ2l0cywgbmFtZSkgew0KICANCiAgZGF0YV9ydW5zIDwtIGxpc3QoKQ0KICANCiAgZm9yIChpIGluIDE6bGVuZ3RoKHNpemUpKSB7DQogICAgc2V0LnNlZWQoMTExMTEpDQogICAgZGF0YSA8LSBydW5pZihzaXplW2ldICogMTAsIDAsIDEpDQogICAgbGFiZWxzIDwtIHJvdW5kKHJ1bmlmKHNpemVbaV0sIDAsIDkpLCBkaWdpdHMgPSAwKQ0KICAgIGNhdCgiICBcbiAgXG4jIyAiLCBuYW1lLCAiIHJ1bjogIixmb3JtYXQoc2l6ZVtpXSwgYmlnLm1hcmsgPSAiLCIsIHNjaWVudGlmaWMgPSBGQUxTRSksICIgc2FtcGxlcyAoIiwgZm9ybWF0KHJ1bnNbaV0sIGJpZy5tYXJrID0gIiwiLCBzY2llbnRpZmljID0gRkFMU0UpLCAiIHRpbWVzKSAgXG4gIFxuIiwgc2VwID0gIiIpDQogICAgdGVzdF9jYXNlKGYsIHByZWRzID0gZGF0YSwgbGFiZWxzID0gbGFiZWxzLCBlcHMgPSAxZS0xNSkNCiAgICBjYXQoIiAgXG4iKQ0KICAgIGRhdGFfcnVuc1tbaV1dIDwtIHByaW50X3dlbGwobWljcm9iZW5jaG1hcmsoZihkYXRhLCBsYWJlbHMsIGVwcyA9IDFlLTE1KSwgdGltZXMgPSBydW5zW2ldKSR0aW1lLCBkaWdpdHMgPSBkaWdpdHMpDQogICAgZGF0YV9ydW5zW1tpXV0gPC0gZGF0YS50YWJsZShCZW5jaCA9IGFzLmZhY3RvcihwYXN0ZTAoIlsiLCBpLCAiXSAiLCBmb3JtYXQoc2l6ZVtpXSwgYmlnLm1hcmsgPSAiLCIsIHNjaWVudGlmaWMgPSBGQUxTRSkpKSwgRnVuY3Rpb24gPSBhcy5mYWN0b3IobmFtZSksIFRpbWUgPSBkYXRhX3J1bnNbW2ldXSkNCiAgICBnYyh2ZXJib3NlID0gRkFMU0UpDQogIH0NCiAgDQogIHJldHVybihkYXRhX3J1bnMpDQogIA0KfQ0KDQpiZW5jaF9zaXplIDwtIGMoMTAwLCAxMDAwLCAxMDAwMCwgMTAwMDAwLCAxMDAwMDAwLCAxMDAwMDAwMCkNCmJlbmNoX3J1bnMgPC0gYygxMDAwMCwgNTAwMCwgMTAwMCwgNTAwLCAxMDAsIDUwKQ0KDQpgYGANCg0KYGBge3IgYmVuY2hfc2NhbGUyLCByZXN1bHRzPSJhc2lzIn0NCg0KcnVuMSA8LSBCZW5jaG1hcmtlcihmYXN0ZXIzLCBiZW5jaF9zaXplLCBiZW5jaF9ydW5zLCBteV9kaWdpdHMsICJQdXJlIFIiKQ0KcnVuMiA8LSBCZW5jaG1hcmtlcihmYXN0ZXI0LCBiZW5jaF9zaXplLCBiZW5jaF9ydW5zLCBteV9kaWdpdHMsICJSY3BwIikNCg0KYGBgDQoNCiMgU2NhbGluZyBSZXN1bHRzDQoNCmBgYHtyIGJlbmNoX3NjYWxlM30NCg0KcnVuMV9hbGwgPC0gcmJpbmRsaXN0KHJ1bjEpDQpydW4yX2FsbCA8LSByYmluZGxpc3QocnVuMikNCnJ1bl9hbGwgPC0gcmJpbmQocnVuMV9hbGwsIHJ1bjJfYWxsKQ0KcnVuX2FsbCRSZXBlYXRzIDwtIHJlcChpbnZlcnNlLnJsZShsaXN0KGxlbmd0aHMgPSBiZW5jaF9ydW5zLCB2YWx1ZXMgPSBiZW5jaF9zaXplKSksIDIpDQpydW5fYWxsJE1pbE9icyA8LSAoMTAwMCAvIHJ1bl9hbGwkVGltZSkgKiBydW5fYWxsJFJlcGVhdHMgLyAxMDAwMDAwDQpydW5fdGltZSA8LSBydW5fYWxsWywgbGlzdChxdWFudGlsZShUaW1lLCBwcm9icyA9IDAuMDUpLCBtZWRpYW4oVGltZSksIHF1YW50aWxlKFRpbWUsIHByb2JzID0gMC45NSksIG1lYW4oVGltZSkpLCBieSA9IGxpc3QoRnVuY3Rpb24sIEJlbmNoKV0NCmNvbG5hbWVzKHJ1bl90aW1lKSA8LSBjKCJGdW5jdGlvbiIsICJCZW5jaG1hcmsiLCAiNSUiLCAiNTAlIiwgIjk1JSIsICJNZWFuIikNCnJ1bl90aW1lJGBNaWwuT2JzL3NgIDwtICgxMDAwIC8gcnVuX3RpbWUkTWVhbikgKiBiZW5jaF9zaXplIC8gMTAwMDAwMA0KcnVuX3RpbWUkYDUlYCA8LSBmb3JtYXQocnVuX3RpbWUkYDUlYCwgZGlnaXRzID0gNiwgc2NpZW50aWZpYyA9IEZBTFNFKQ0KcnVuX3RpbWUkYDUwJWAgPC0gZm9ybWF0KHJ1bl90aW1lJGA1MCVgLCBkaWdpdHMgPSA2LCBzY2llbnRpZmljID0gRkFMU0UpDQpydW5fdGltZSRgOTUlYCA8LSBmb3JtYXQocnVuX3RpbWUkYDk1JWAsIGRpZ2l0cyA9IDYsIHNjaWVudGlmaWMgPSBGQUxTRSkNCnJ1bl90aW1lJE1lYW4gPC0gZm9ybWF0KHJ1bl90aW1lJE1lYW4sIGRpZ2l0cyA9IDYsIHNjaWVudGlmaWMgPSBGQUxTRSkNCnJ1bl90aW1lJGBNaWwuT2JzL3NgIDwtIGZvcm1hdChydW5fdGltZSRgTWlsLk9icy9zYCwgZGlnaXRzID0gNiwgc2NpZW50aWZpYyA9IEZBTFNFKQ0KDQpwcmludChydW5fdGltZVsxOihucm93KHJ1bl90aW1lKSAvIDIpXSkNCnByaW50KHJ1bl90aW1lWyhucm93KHJ1bl90aW1lKSAvIDIgKyAxKTpucm93KHJ1bl90aW1lKV0pDQoNCmBgYA0KDQpgYGB7ciBiZW5jaF9zY2FsZTQsIGZpZy5oZWlnaHQ9NiwgZmlnLndpZHRoPTEwfQ0KDQpnZ3Bsb3QocnVuX2FsbCwgYWVzKHggPSBCZW5jaCwgeSA9IFRpbWUsIGNvbG9yID0gRnVuY3Rpb24sIGZpbGwgPSBCZW5jaCkpICsgZ2VvbV9ib3hwbG90KCkgKyBzY2FsZV95X2xvZzEwKGxhYmVscyA9IHNjYWxlczo6Y29tbWEsIGJyZWFrcyA9IGMoMC4wMSwgMC4xLCAxLCAxMCwgMTAwLCAxMDAwLCAxMDAwMCkpICsgc3RhdF9zdW1tYXJ5KGZ1bi55ID0gbWVhbiwgZ2VvbSA9ICJsaW5lIiwgYWVzKGdyb3VwID0gRnVuY3Rpb24pKSArIHN0YXRfc3VtbWFyeShmdW4ueSA9IG1lYW4sIGdlb20gPSAicG9pbnQiLCBhZXMoZ3JvdXAgPSBGdW5jdGlvbikpICsgbGFicyh4ID0gIkJlbmNobWFyayIsIHkgPSAiVGltZSAoTWlsbGlzZWNvbmRzKSIpICsgdGhlbWVfYncoKQ0KDQpgYGANCg0KYGBge3IgYmVuY2hfc2NhbGU1LCBmaWcuaGVpZ2h0PTYsIGZpZy53aWR0aD0xMH0NCg0KZ2dwbG90KHJ1bl9hbGwsIGFlcyh4ID0gQmVuY2gsIHkgPSBNaWxPYnMsIGNvbG9yID0gRnVuY3Rpb24sIGZpbGwgPSBCZW5jaCkpICsgZ2VvbV9ib3hwbG90KCkgKyBzY2FsZV95X2xvZzEwKGxhYmVscyA9IHNjYWxlczo6Y29tbWEsIGJyZWFrcyA9IGMoMSwgMi41LCA1LCA3LjUsIDEwLCAxMi41LCAxNSwgMTcuNSwgMjAsIDIyLjUsIDI1KSwgbGltaXRzID0gYygxLCBOQSkpICsgc3RhdF9zdW1tYXJ5KGZ1bi55ID0gbWVhbiwgZ2VvbSA9ICJsaW5lIiwgYWVzKGdyb3VwID0gRnVuY3Rpb24pKSArIHN0YXRfc3VtbWFyeShmdW4ueSA9IG1lYW4sIGdlb20gPSAicG9pbnQiLCBhZXMoZ3JvdXAgPSBGdW5jdGlvbikpICsgbGFicyh4ID0gIkJlbmNobWFyayIsIHkgPSAiVGhyb3VnaHB1dCAoTWlsbGlvbiBPYnMuL3MpIikgKyB0aGVtZV9idygpDQoNCmBgYA0KDQpgYGB7ciBiZW5jaF9zY2FsZTYsIGZpZy5oZWlnaHQ9NiwgZmlnLndpZHRoPTEwfQ0KDQpnZ3Bsb3QoZGF0YSA9IHJ1bl9hbGwsIGFlcyh4ID0gTWlsT2JzLCBjb2xvciA9IEZ1bmN0aW9uLCBmaWxsID0gRnVuY3Rpb24sIGdyb3VwID0gRnVuY3Rpb24pKSArIGNvb3JkX2ZsaXAoKSArIHN0YXRfZWNkZihhZXMoeW1pbiA9IC4ueS4uLCB5bWF4ID0gMSksIGFscGhhID0gMC41LCBnZW9tID0gInJpYmJvbiIpICsgc3RhdF9lY2RmKGdlb20gPSAibGluZSIsIHNpemUgPSAyLCBhbHBoYSA9IDAuNzUsIHBhZCA9IEZBTFNFKSArIGxhYnMoeCA9ICJUaHJvdWdocHV0IChNaWxsaW9uIE9icy4vcykiLCB5ID0gIlBlcmNlbnRpbGUiKSArIGZhY2V0X3dyYXAofiBCZW5jaCwgZGlyID0gImgiLCBuY29sID0gMiwgc2NhbGVzID0gImZyZWUiKSArIHRoZW1lX2J3KCkNCg0KYGBgDQo=